/**
* file name  : it/ciano/jenoware/Family.java
* authors    : Luciano Xumerle
* created    : gio 16 dic 2004 18:31:59 CET
*
* Copyright (c) 2004-2010 Luciano Xumerle. All rights reserved.
*
* This file is part of gephinfo.
*
* gephinfo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*/

package it.ciano.jenoware;

import java.util.*;

/**
 * The class manages the Sbj objects of one family.
 * The class extends HashMap: the keys are the unique identifier of
 * the Sbj objects and the values are the Sbj objects.
 *
 * @author Luciano Xumerle
 * @version 0.0.1
 */
final public class Family extends HashMap {

    static final long serialVersionUID = -4753063225161420880L;

    /**
     * First Degree: Parent-Child
     */
    final public static char FD_ParentChild ='p';

    /**
     * First Degree: Full Sibs
     */
    final public static char FD_FullSibs ='b';

    /**
     * Second Degree: Uncle
     */
    final public static char SD_Uncle ='u';

    /**
     * Second Degree: GrandFather
     */
    final public static char SD_GrandFather ='g';

    /**
     * Third Degree: cousin
     */
    final public static char TD_cousin ='c';

    /**
     * No parentship detected.
     */
    final public static char None ='n';


    private String _fml;
    private HashMap _pos;
    private HashMap _errors;

    /**
     * Class constructor.
     */
    public Family() {
        this ( "" );
    }

    /**
     * Class constructor.
     *
     * @param familyNumber the unique number of the family.
     */
    public Family ( String familyNumber ) {
        super();
        if ( familyNumber == null )
            _fml = "";
        else
            _fml = familyNumber;
        _pos = new HashMap();
        _errors = new HashMap();
    }

    /**
     * Returns the unique identifier of the Family.
     *
     * @return the unique identifier of the Family.
     */
    public String getFamilyID() {
        return _fml;
    }


    /**
     * Sets the unique identifier of the Family.
     *
     * @param familyID the family ID.
     */
    public void setFamilyID ( String familyID ) {
        _fml=familyID;
    }


    /**
     * Returns true if the given Sbj's ID is stored in the Family object.
     *
     * @param id the ID of a Subject.
     * @return true if the ID is stored.
     */
    public boolean exist ( String id ) {
        return containsKey ( id );
    }

    /**
     * Adds the Sbj object to the Family object.
     *
     * @param s the Sbj object.
     * @return true or false.
     */
    public boolean addSbj ( Sbj s ) {
        if ( s != null && s.existFamily ( _fml ) ) {
            put ( s.getID(), s );
            if (  !s.getSBJ ( _fml ).equals ( "" )  && !s.getSBJ ( _fml ).equals ( "0" ) )
                _pos.put ( s.getSBJ ( _fml ), s.getID() );
            return true;
        }
        return false;
    }

    /**
     * Adds the faked Sbj object to this Pedigree.
     * A faked Sbj is added automatically to complete a family (ie. missing parent).
     *
     * @param entry the Sbj object.
     * @return true or false.
     */
    public boolean addSbjAsFaked ( Sbj entry ) {
        if ( entry == null )
            return false;
        entry.setAsFaked();
        return addSbj ( entry );
    }

    /**
     * Returns the Sbj object for the given ID (null if not exist).
     *
     * @param id the Sbj's ID.
     * @return the Sbj object (null if not exist).
     */
    public Sbj getSBJ ( String id ) {
        if ( id != null && containsKey ( id ) )
            return ( Sbj ) this.get ( id );
        return null;
    }

    /**
     * Returns the ID of the Sbj by the given family Sbj unique number.
     *
     * @param famPosition the family Sbj unique number.
     * @return the Sbj's ID String (null if there is no value).
     */
    public String getSbjID ( String famPosition ) {
        if ( famPosition != null && _pos.containsKey ( famPosition ) )
            return ( String ) _pos.get ( famPosition );
        return null;
    }

    /**
     * Returns the Sbj unique number of the Sbj with the given ID.
     *
     * @param id the Sbj's ID.
     * @return the Sbj  unique number (null if there is no value).
     */
    public String getSbjNumber ( String id ) {
        if ( id != null && containsKey ( id ) )
            return ( ( Sbj ) this.get ( id ) ).getSBJ ( _fml );
        return null;
    }

    /**
     * Returns a String array containing every ID contained in the Family object.
     *
     * @return String array.
     */
    public String[] getSbjIDs() {
        Sbj[] aa = getSbjs();
        if ( aa == null )
            return null;
        String tt[] = new String[ aa.length ];
        for ( int i = 0; i < aa.length; i++ )
            tt[ i ] = aa[ i ].getID();
        return tt;
    }

    /**
     * Returns an array with every Sbj object in the Family object.
     *
     * @return a Sbj array.
     */
    public Sbj[] getSbjs() {
        ArrayList pp = getListSbj();
        if ( pp.size() > 0 ) {
            Collections.sort ( pp );
            Sbj aa[] = new Sbj[ pp.size() ];
            for ( int i = 0; i < pp.size(); i++ )
                aa[ i ] = ( Sbj ) pp.get ( i );
            Arrays.sort ( aa );
            return aa;
        }
        return null;
    }

    /**
     * Returns the Sbj object set as proband.
     *
     * @return the proband (null if none exist).
     */
    public Sbj getProband() {
        ArrayList pp = getListSbj();
        if ( pp.size() > 0 ) {
            for ( int i = 0; i < pp.size(); i++ ) {
                Sbj tt = ( Sbj ) pp.get ( i );
                if ( tt.getProband ( _fml ).equals ( "1" ) )
                    return tt;
            }
        }
        return null;
    }

    /**
      * Returns an array of two elements {"father", "mother"}.
      * If father (or mother) doesn't exist, then creates the needed faked Sbj.
      *
      * @param sbj the given Sbj.
      * @return the Sbj array (null if the given Sbj is null or faked).
      */
    public Sbj[] getParents ( Sbj sbj ) {
        if ( sbj == null || sbj.isFaked() )
            return null;
        Sbj ret[] = new Sbj[ 2 ];
        ret[ 0 ] = getSBJ ( getSbjID ( sbj.getFTH ( _fml ) ) );
        ret[ 1 ] = getSBJ ( getSbjID ( sbj.getMTH ( _fml ) ) );
        if ( ret[ 0 ] == null )
            ret[ 0 ] = sbj.getFakedParentSBJ ( _fml, "1" );
        if ( ret[ 1 ] == null )
            ret[ 1 ] = sbj.getFakedParentSBJ ( _fml, "2" );
        return ret;
    }

    /**
     * Returns an array with the Genotype of the father and the mother.
     *
     * @param sbj The given Sbj.
     * @param marker the marker name.
     * @return array with {"father's Genotype", "mother's Genotype"}.
     */
    public Genotype[] getParentsMarker ( Sbj sbj, String marker ) {
        Sbj ret[] = getParents ( sbj );
        if ( ret == null )
            return new Genotype[] { new Genotype ( marker ), new Genotype ( marker ) };
        return new Genotype[] { ret[ 0 ].getGenotype ( marker ),
                                ret[ 1 ].getGenotype ( marker )
                              };
    }

    /**
     * Returns an array with the Genotype of every possible brothers of the subject.
     *
     * @param sbj The given Sbj.
     * @param marker the marker name.
     * @return array with {"son1 genotype","son2 g.","son3 g.","son4 g."}.
     */
    public Genotype[] getPossibleBros ( Sbj sbj, String marker ) {
        Sbj ret[] = getParents ( sbj );
        Genotype f = ret[ 0 ].getGenotype ( marker );
        Genotype m = ret[ 1 ].getGenotype ( marker );
        Genotype r[] = new Genotype[ 4 ];
        r[ 0 ] = new Genotype ( marker, f.getAllele ( 1 ), m.getAllele ( 1 ) );
        r[ 1 ] = new Genotype ( marker, f.getAllele ( 1 ), m.getAllele ( 2 ) );
        r[ 2 ] = new Genotype ( marker, f.getAllele ( 2 ), m.getAllele ( 1 ) );
        r[ 3 ] = new Genotype ( marker, f.getAllele ( 2 ), m.getAllele ( 2 ) );
        return r;
    }

    /**
     * Returns a Sbj array containing the Sbj's brothers.
     *
     * @param sbj the Sbj.
     * @return the Sbj array (null if there is no brother).
     */
    public Sbj[] getBros ( Sbj sbj ) {
        ArrayList bros = getBrosAL ( sbj );
        if ( bros.size() > 0 ) {
            Sbj pp[] = new Sbj[ bros.size() ];
            for ( int i = 0; i < bros.size(); i++ )
                pp[ i ] = ( Sbj ) bros.get ( i );
            return pp;
        }
        return null;
    }

    /**
     * Returns true if the Sbj has almost one brother.
     *
     * @param sbj the subject.
     * @return true or false.
     */
    public boolean hasBros ( Sbj sbj ) {
        return ( getBrosAL ( sbj ).size() > 0 );
    }

    /**
     * Returns an ArrayList of Sbj objects containing the brothers of the Sbj.
     *
     * @param sbj the subject.
     * @return an ArrayList of Sbj objects.
     */
    private ArrayList getBrosAL ( Sbj sbj ) {
        ArrayList bros = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj tt = ( Sbj ) entry.getValue();
            if ( tt != null && !sbj.getID().equals ( tt.getID() )
                    && tt.getFTH ( _fml ).equals ( sbj.getFTH ( _fml ) )
                    && tt.getMTH ( _fml ).equals ( sbj.getMTH ( _fml ) ) )
                bros.add ( tt );
        }
        return bros;
    }

    /**
     * Returns a Sbj array containing the sons of the given Sbj.
     *
     * @param sbj the subject.
     * @return the String array (null if there is no son).
     */
    public Sbj[] getSons ( Sbj sbj ) {
        ArrayList son = getSonsAL ( sbj );
        if ( son.size() > 0 ) {
            Sbj pp[] = new Sbj[ son.size() ];
            for ( int i = 0; i < son.size(); i++ )
                pp[ i ] = ( Sbj ) son.get ( i );
            return pp;
        }
        return null;
    }

    /**
     * Returns true if the one subject is the son of the other subject.
     *
     * @return true or false.
     */
    public boolean isSon ( Sbj a, Sbj b ) {
        return (  a.getFTH ( _fml ).equals ( b.getSBJ ( _fml ) )
                  || a.getMTH ( _fml ).equals ( b.getSBJ ( _fml ) )
                  || b.getFTH ( _fml ).equals ( a.getSBJ ( _fml ) )
                  || b.getMTH ( _fml ).equals ( a.getSBJ ( _fml ) )
               );
    }

    /**
     * Returns true if the Sbj has almost one son.
     *
     * @param sbj the subject.
     * @return true or false.
     */
    public boolean hasSons ( Sbj sbj ) {
        return ( getSonsAL ( sbj ).size() > 0 );
    }

    /**
     * Returns an ArrayList of Sbj objects containing the sons of the Sbj.
     *
     * @param sbj a parents.
     * @return an ArrayList of Sbj objects.
     */
    private ArrayList getSonsAL ( Sbj sbj ) {
        ArrayList sons = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj tt = ( Sbj ) entry.getValue();
            if ( tt != null &&
                    ( tt.getFTH ( _fml ).equals ( sbj.getSBJ ( _fml ) )
                      || tt.getMTH ( _fml ).equals ( sbj.getSBJ ( _fml ) ) ) )
                sons.add ( tt );
        }
        return sons;
    }

    /**
     * Returns an Allele array containing every valid allele observed in the family.
     *
     * @param marker the marker name.
     * @return Allele array.
     */
    final  public Allele[] getObservedAlleles ( String marker ) {
        return Genotype.getObservedAlleles ( getObservedGenotypes ( marker ) );
    }

    /**
     * Returns a Genotype array containing every valid genotype observed in the family.
     *
     * @param marker the marker name.
     * @return Genotype array.
     */
    final  public Genotype[] getObservedGenotypes ( String marker ) {
        HashSet res = new HashSet();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj tt = ( Sbj ) entry.getValue();
            if ( tt == null )
                continue;
            Genotype gg = tt.getGenotype ( marker );
            if ( gg != null && gg.isValid() )
                res.add ( gg );
        }
        if ( res.size() > 0 ) {
            Genotype rr[] = new Genotype[ res.size() ];
            int i = 0;
            for ( Iterator it = res.iterator(); it.hasNext(); )
                rr[ i++ ] = ( Genotype ) it.next();
            Arrays.sort ( rr );
            return rr;
        }
        return null;
    }

    /**
     * Returns an ArrayList containing every Sbj object stored in the Family object (without fakes).
     *
     * @return an ArrayList containing Sbj objects (without faked Sbj).
     */
    public ArrayList getListSbj() {
        ArrayList pp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj tt = ( Sbj ) entry.getValue();
            if ( !tt.isFaked() )
                pp.add ( tt );
        }
        return pp;
    }

    /**
     * Returns an ArrayList containing only the faked Sbj objects stored in the Family object.
     *
     * @return an ArrayList containing faked Sbj objects.
     */
    public ArrayList getListFakedSbj() {
        ArrayList pp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj tt = ( Sbj ) entry.getValue();
            if ( tt.isFaked() )
                pp.add ( tt );
        }
        return pp;
    }

    /**
     * Returns an ArrayList containing only the genotyped Sbj objects stored in the Family object.
     *
     * @return an ArrayList containing Genotyped Sbj objects.
     */
    public ArrayList getGenotypedSbj() {
        ArrayList pp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj tt = ( Sbj ) entry.getValue();
            if ( tt.isGenotyped() )
                pp.add ( tt );
        }
        return pp;
    }

    /**
     * Returns an ArrayList containing only the unrelated Sbj objects stored in the Family object.
     * An unrelated Sbj is genotiped for some marker, has no brother and has no
     * genotyped parents. Sometimes a Sbj is related because the parents are genotyped,
     * but, for some marker, the Sbj is not related. The method don't recognize it!
     *
     * @return an ArrayList containing unrelated Sbj objects.
     */
    public ArrayList getUnrelatedSbj() {
        ArrayList temp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj oo = ( Sbj ) entry.getValue();
            Sbj[] pp = getParents ( oo );
            // if fth == mth then is mth=0=fth -->  unrelated ID
            // OR (there are no genotiped parents and no brother)
            if ( oo.isGenotyped()
                    && (
                        // no parents
                        oo.getFTH ( _fml ).equals ( oo.getMTH ( _fml ) )
                        // no genotyped parents and no brothers
                        || ( ! ( ( pp[ 0 ] != null && pp[ 0 ].isGenotyped() )
                                 || ( pp[ 1 ] != null && pp[ 1 ].isGenotyped() )
                                 || hasBros ( oo ) ) ) )
               )
                temp.add ( oo );
        }
        return temp;
    }

    /**
     * Returns the parents relationship between two family members.
     * <ul>
     * <li>p : parent-child</li>
     * <li>b : brothers</li>
     * <li>g : grandfather (second-degree)</li>
     * <li>u : uncle (second-degree)</li>
     * <li>c : cousins (third-degree)</li>
     * <li>n : none of above</li>
     * </ul>
     *
     * @param a the first family member.
     * @param b the second family member.
     * @return [p]arent-child, [c]ousins, [b]rothers, [g]randfather, [u]ncle or [n]one.
     */
    final public char getRelationship ( Sbj a, Sbj b  ) {
        if ( !a.getFML().equals ( b.getFML() ) )
            return None;

        /**
        * first degree: parent-child
        */
        if (  a.getSBJ ( _fml ).equals ( b.getFTH ( _fml ) )
                || a.getSBJ ( _fml ).equals ( b.getMTH ( _fml ) )
                || b.getSBJ ( _fml ).equals ( a.getFTH ( _fml ) )
                || b.getSBJ ( _fml ).equals ( a.getMTH ( _fml ) ) )
            return FD_ParentChild;

        /**
         * first degree: brothers
         */
        if ( isBros ( a, b ) )
            return FD_FullSibs;

        // get parents to obtain the other relationships
        Sbj[] ap=getParents ( a );
        Sbj[] bp=getParents ( b );

        /**
         * second degree: uncle
        */
        if ( isBros ( ap[0], b ) || isBros ( ap[1], b )
                || isBros ( bp[0], a ) ||  isBros ( bp[1], a ) )
            return SD_Uncle;

        /**
        * second degree: grandfather
        */
        if ( isSon ( ap[0], b )
                || isSon ( ap[1], b )
                || isSon ( bp[0], a )
                || isSon ( bp[1], a ) )
            return SD_GrandFather;

        /**
         * third degree : cousins
         */
        if ( isBros ( ap[0],bp[0] ) ||  isBros ( ap[1],bp[0] )
                || isBros ( ap[0],bp[1] ) || isBros ( ap[1],bp[1] )  )
            return TD_cousin;

        return None;
    }

    /**
     * Returns true if the subjects have the same father and mother.
     *
     * @param a the first family member.
     * @param b the second family member.
     * @return true or false.
     */
    final public boolean isBros ( Sbj a, Sbj b ) {
        return ( !a.getFTH ( _fml ).equals ( a.getMTH ( _fml ) )
                 &&  a.getFTH ( _fml ).equals ( b.getFTH ( _fml ) )
                 && a.getMTH ( _fml ).equals ( b.getMTH ( _fml ) ) );
    }

    /**
      * The method does a mendelian check for a given marker.
      *
      * @param marker the marker name.
      * @return the results (an empty String if there is no errors).
      */
    public String pedCheck ( String marker ) {
        StringBuffer res = new StringBuffer();
        res.append ( "===> Error with family: " );
        res.append ( _fml );
        res.append ( " [ Marker: " );
        res.append ( marker );
        res.append ( " ]\n" );
        int ret = res.length();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            HashMap alleles = new HashMap();
            boolean hasError = false;
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj ss = ( Sbj ) entry.getValue();
            String key = getKey ( ss.getFTH ( _fml ), ss.getMTH ( _fml ), marker );
            Sbj[] pp = getParents ( ss );
            Genotype gg = ss.getGenotype ( marker );
            if ( gg.isValid() ) {
                alleles.put ( gg.getAll1(), gg.getAllele ( 1 ) );
                alleles.put ( gg.getAll2(), gg.getAllele ( 2 ) );
            }
            ArrayList bros = getBrosAL ( ss );
            for ( int i = 0; i < bros.size(); i++ ) {
                Sbj cc = ( Sbj ) bros.get ( i );
                Genotype v = cc.getGenotype ( marker );
                if ( v.isValid() ) {
                    alleles.put ( v.getAll1(), v.getAllele ( 1 ) );
                    alleles.put ( v.getAll2(), v.getAllele ( 2 ) );
                }
            }

            if ( !_errors.containsKey ( key ) ) {
                if ( !gg.isSon ( pp[ 0 ].getGenotype ( marker ),
                                 pp[ 1 ].getGenotype ( marker ) ) ) {
                    res.append ( " ## Segregation errors:\n" );
                    hasError = true;

                }
                if ( alleles.size() > 4 ) {
                    res.append ( "** The sons show " );
                    res.append ( alleles.size() );
                    res.append ( " alleles\n" );
                    hasError = true;
                }
            }
            if ( hasError ) {
                _errors.put ( key, key );
                // father info
                res.append ( "    + fath " );
                res.append ( pp[ 0 ].getGenotypeInfo ( marker ) );
                res.append ( "\n" );
                // mother info
                res.append ( "    o moth " );
                res.append ( pp[ 1 ].getGenotypeInfo ( marker ) );
                res.append ( "\n" );
                // sons info
                res.append ( "    - son1 " );
                res.append ( ss.getGenotypeInfo ( marker ) );
                res.append ( "\n" );

                for ( int j = 0; j < bros.size(); j++ ) {
                    Sbj bb = ( Sbj ) bros.get ( j );
                    Genotype ll = bb.getGenotype ( marker );
                    if ( !ll.isSon ( pp[ 0 ].getGenotype ( marker ),
                                     pp[ 1 ].getGenotype ( marker ) ) )
                        res.append ( "    - " );
                    else
                        res.append ( "    . " );
                    res.append ( "son" );
                    res.append ( j + 2 );
                    res.append ( " " );
                    res.append ( bb.getGenotypeInfo ( marker ) );
                    res.append ( "\n" );
                }
                return res.toString();
            }
        }
        if ( ret < res.length() )
            return res.toString();
        return "";
    }

    /**
     * Creates the key needed to store segregation errors.
     *
     * @param fth father unique family number.
     * @param mth mother unique family number.
     * @param marker name of the marker.
     * @return the key needed to store segregation errors.
     */
    static String getKey ( String fth, String mth, String marker ) {
        StringBuffer tt = new StringBuffer();
        tt.append ( fth );
        tt.append ( "#" );
        tt.append ( mth );
        tt.append ( "#" );
        tt.append ( marker );
        return tt.toString();
    }

} // end class

